package io.github.center.config.typehandler;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import io.github.common.enums.SerializeEnum;
import lombok.NoArgsConstructor;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>Description:  </p>
 *
 * @param <E> parameter
 * @author haijun
 * @version 1.0.0
 * @email "1424132610@qq.com"
 * @date 2023.10.15 20:54
 * @since 1.0.0
 */
public class CommonEnumTypeHandler<E extends SerializeEnum> extends BaseTypeHandler<E> {
    /**
     * TABLE_METHOD_OF_ENUM_TYPES
     */
    private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();

    /**
     * REFLECTOR_FACTORY
     */
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();

    /**
     * Enum class type
     */
    private final Class<E> enumClassType;

    /**
     * 当前值的字段类型
     */
    private final Class<?> propertyType;

    /**
     * mybatis plus封装的获取值的执行器
     */
    private final Invoker getInvoker;

    /**
     * Common enum type handler
     *
     * @param enumClassType enum class type
     * @since 1.0.0
     */
    public CommonEnumTypeHandler(Class<E> enumClassType) {
        if (enumClassType == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        } else {
            //获取到前字段的枚举类型
            this.enumClassType = enumClassType;
            //获取到枚举类型
            MetaClass metaClass = MetaClass.forClass(enumClassType, REFLECTOR_FACTORY);
            String name = SerializeEnum.VALUE_FILED_NAME;
            //如果枚举类型不为 CommonEnum 接口的实现类，
            if (!SerializeEnum.class.isAssignableFrom(enumClassType)) {
                name = findEnumValueFieldName(this.enumClassType)
                        .orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.",
                                this.enumClassType.getName())));
            }
            //通过getter方法来获取到对于属性值的类型
            this.propertyType = ReflectionKit.resolvePrimitiveIfNecessary(metaClass.getGetterType(name));
            this.getInvoker = metaClass.getGetInvoker(name);
        }
    }

    /**
     * 根据枚举类型来获取对应字段的 @EnumValue 注解的值
     *
     * @param clazz clazz
     * @return optional optional
     * @since 1.0.0
     */
    public static Optional<String> findEnumValueFieldName(Class<?> clazz) {
        if (clazz != null && clazz.isEnum()) {
            String className = clazz.getName();
            return Optional.ofNullable(CollectionUtils.computeIfAbsent(TABLE_METHOD_OF_ENUM_TYPES, className, (key) -> {
                Optional<Field> fieldOptional = findEnumValueAnnotationField(clazz);
                return fieldOptional.map(Field::getName).orElse(null);
            }));
        } else {
            return Optional.empty();
        }
    }

    /**
     * 处理字段上是否设置了 EnumValue
     *
     * @param clazz clazz
     * @return optional optional
     * @since 1.0.0
     */
    private static Optional<Field> findEnumValueAnnotationField(Class<?> clazz) {
        return Arrays.stream(clazz.getDeclaredFields()).filter((field) -> field.isAnnotationPresent(EnumValue.class)).findFirst();
    }

    /**
     * 判断是否是CommonEnum的接口实现
     *
     * @param clazz clazz
     * @return boolean boolean
     * @since 1.0.0
     */
    public static boolean isMpEnums(Class<?> clazz) {
        return clazz != null && clazz.isEnum() && (SerializeEnum.class.isAssignableFrom(clazz) || findEnumValueFieldName(clazz).isPresent());
    }

    /**
     * Sets non null parameter *
     *
     * @param ps        ps
     * @param i
     * @param parameter parameter
     * @param jdbcType  jdbc type
     * @throws SQLException sql exception
     * @since 1.0.0
     */
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        if (jdbcType == null) {
            ps.setObject(i, this.getValue(parameter));
        } else {
            ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
        }

    }

    /**
     * Gets nullable result *
     *
     * @param rs         rs
     * @param columnName column name
     * @return the nullable result
     * @throws SQLException sql exception
     * @since 1.0.0
     */
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        Object value = rs.getObject(columnName, this.propertyType);
        return null == value && rs.wasNull() ? null : this.valueOf(value);
    }

    /**
     * Gets nullable result *
     *
     * @param rs          rs
     * @param columnIndex column index
     * @return the nullable result
     * @throws SQLException sql exception
     * @since 1.0.0
     */
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        Object value = rs.getObject(columnIndex, this.propertyType);
        return null == value && rs.wasNull() ? null : this.valueOf(value);
    }

    /**
     * Gets nullable result *
     *
     * @param cs          cs
     * @param columnIndex column index
     * @return the nullable result
     * @throws SQLException sql exception
     * @since 1.0.0
     */
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        Object value = cs.getObject(columnIndex, this.propertyType);
        return null == value && cs.wasNull() ? null : this.valueOf(value);
    }

    /**
     * Value of
     *
     * @param value value
     * @return the e
     * @since 1.0.0
     */
    private E valueOf(Object value) {
        E[] es = (E[]) this.enumClassType.getEnumConstants();
        return (E) Arrays.stream(es).filter((e) -> this.equalsValue(value, this.getValue(e))).findAny().orElse((E) null);
    }

    /**
     * Equals value
     *
     * @param sourceValue source value
     * @param targetValue target value
     * @return the boolean
     * @since 1.0.0
     */
    protected boolean equalsValue(Object sourceValue, Object targetValue) {
        String sValue = StringUtils.toStringTrim(sourceValue);
        String tValue = StringUtils.toStringTrim(targetValue);
        return sourceValue instanceof Number
                &&
                targetValue instanceof Number
                && (new BigDecimal(sValue)).compareTo(new BigDecimal(tValue)) == 0 || Objects.equals(sValue, tValue);
    }

    /**
     * Gets value *
     *
     * @param object object
     * @return the value
     * @since 1.0.0
     */
    private Object getValue(Object object) {
        try {
            return this.getInvoker.invoke(object, new Object[0]);
        } catch (ReflectiveOperationException var3) {
            throw ExceptionUtils.mpe(var3);
        }
    }
}
